/*
 * Copyright 2002-2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.jndi.support;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanDefinitionStoreException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanNotOfRequiredTypeException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.core.ResolvableType;
import org.springframework.jndi.JndiLocatorSupport;
import org.springframework.jndi.TypeMismatchNamingException;
import org.springframework.lang.Nullable;

import javax.naming.NameNotFoundException;
import javax.naming.NamingException;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * Simple JNDI-based implementation of Spring's
 * {@link org.springframework.beans.factory.BeanFactory} interface.
 * Does not support enumerating bean definitions, hence doesn't implement
 * the {@link org.springframework.beans.factory.ListableBeanFactory} interface.
 *
 * <p>This factory resolves given bean names as JNDI names within the
 * Java EE application's "java:comp/env/" namespace. It caches the resolved
 * types for all obtained objects, and optionally also caches shareable
 * objects (if they are explicitly marked as
 * {@link #addShareableResource shareable resource}.
 *
 * <p>The main intent of this factory is usage in combination with Spring's
 * {@link org.springframework.context.annotation.CommonAnnotationBeanPostProcessor},
 * configured as "resourceFactory" for resolving {@code @Resource}
 * annotations as JNDI objects without intermediate bean definitions.
 * It may be used for similar lookup scenarios as well, of course,
 * in particular if BeanFactory-style type checking is required.
 *
 * @author Juergen Hoeller
 * @see org.springframework.beans.factory.support.DefaultListableBeanFactory
 * @see org.springframework.context.annotation.CommonAnnotationBeanPostProcessor
 * @since 2.5
 */
public class SimpleJndiBeanFactory extends JndiLocatorSupport implements BeanFactory {

    /**
     * JNDI names of resources that are known to be shareable, i.e. can be cached
     */
    private final Set<String> shareableResources = new HashSet<>();

    /**
     * Cache of shareable singleton objects: bean name to bean instance.
     */
    private final Map<String, Object> singletonObjects = new HashMap<>();

    /**
     * Cache of the types of nonshareable resources: bean name to bean type.
     */
    private final Map<String, Class<?>> resourceTypes = new HashMap<>();


    public SimpleJndiBeanFactory() {
        setResourceRef(true);
    }


    /**
     * Add the name of a shareable JNDI resource,
     * which this factory is allowed to cache once obtained.
     *
     * @param shareableResource the JNDI name
     *                          (typically within the "java:comp/env/" namespace)
     */
    public void addShareableResource(String shareableResource) {
        this.shareableResources.add(shareableResource);
    }

    /**
     * Set a list of names of shareable JNDI resources,
     * which this factory is allowed to cache once obtained.
     *
     * @param shareableResources the JNDI names
     *                           (typically within the "java:comp/env/" namespace)
     */
    public void setShareableResources(String... shareableResources) {
        Collections.addAll(this.shareableResources, shareableResources);
    }


    //---------------------------------------------------------------------
    // Implementation of BeanFactory interface
    //---------------------------------------------------------------------


    @Override
    public Object getBean(String name) throws BeansException {
        return getBean(name, Object.class);
    }

    @Override
    public <T> T getBean(String name, Class<T> requiredType) throws BeansException {
        try {
            if (isSingleton(name)) {
                return doGetSingleton(name, requiredType);
            } else {
                return lookup(name, requiredType);
            }
        } catch (NameNotFoundException ex) {
            throw new NoSuchBeanDefinitionException(name, "not found in JNDI environment");
        } catch (TypeMismatchNamingException ex) {
            throw new BeanNotOfRequiredTypeException(name, ex.getRequiredType(), ex.getActualType());
        } catch (NamingException ex) {
            throw new BeanDefinitionStoreException("JNDI environment", name, "JNDI lookup failed", ex);
        }
    }

    @Override
    public Object getBean(String name, @Nullable Object... args) throws BeansException {
        if (args != null) {
            throw new UnsupportedOperationException(
                    "SimpleJndiBeanFactory does not support explicit bean creation arguments");
        }
        return getBean(name);
    }

    @Override
    public <T> T getBean(Class<T> requiredType) throws BeansException {
        return getBean(requiredType.getSimpleName(), requiredType);
    }

    @Override
    public <T> T getBean(Class<T> requiredType, @Nullable Object... args) throws BeansException {
        if (args != null) {
            throw new UnsupportedOperationException(
                    "SimpleJndiBeanFactory does not support explicit bean creation arguments");
        }
        return getBean(requiredType);
    }

    @Override
    public <T> ObjectProvider<T> getBeanProvider(Class<T> requiredType) {
        return new ObjectProvider<T>() {
            @Override
            public T getObject() throws BeansException {
                return getBean(requiredType);
            }

            @Override
            public T getObject(Object... args) throws BeansException {
                return getBean(requiredType, args);
            }

            @Override
            @Nullable
            public T getIfAvailable() throws BeansException {
                try {
                    return getBean(requiredType);
                } catch (NoUniqueBeanDefinitionException ex) {
                    throw ex;
                } catch (NoSuchBeanDefinitionException ex) {
                    return null;
                }
            }

            @Override
            @Nullable
            public T getIfUnique() throws BeansException {
                try {
                    return getBean(requiredType);
                } catch (NoSuchBeanDefinitionException ex) {
                    return null;
                }
            }
        };
    }

    @Override
    public <T> ObjectProvider<T> getBeanProvider(ResolvableType requiredType) {
        throw new UnsupportedOperationException(
                "SimpleJndiBeanFactory does not support resolution by ResolvableType");
    }

    @Override
    public boolean containsBean(String name) {
        if (this.singletonObjects.containsKey(name) || this.resourceTypes.containsKey(name)) {
            return true;
        }
        try {
            doGetType(name);
            return true;
        } catch (NamingException ex) {
            return false;
        }
    }

    @Override
    public boolean isSingleton(String name) throws NoSuchBeanDefinitionException {
        return this.shareableResources.contains(name);
    }

    @Override
    public boolean isPrototype(String name) throws NoSuchBeanDefinitionException {
        return !this.shareableResources.contains(name);
    }

    @Override
    public boolean isTypeMatch(String name, ResolvableType typeToMatch) throws NoSuchBeanDefinitionException {
        Class<?> type = getType(name);
        return (type != null && typeToMatch.isAssignableFrom(type));
    }

    @Override
    public boolean isTypeMatch(String name, @Nullable Class<?> typeToMatch) throws NoSuchBeanDefinitionException {
        Class<?> type = getType(name);
        return (typeToMatch == null || (type != null && typeToMatch.isAssignableFrom(type)));
    }

    @Override
    @Nullable
    public Class<?> getType(String name) throws NoSuchBeanDefinitionException {
        try {
            return doGetType(name);
        } catch (NameNotFoundException ex) {
            throw new NoSuchBeanDefinitionException(name, "not found in JNDI environment");
        } catch (NamingException ex) {
            return null;
        }
    }

    @Override
    public String[] getAliases(String name) {
        return new String[0];
    }


    @SuppressWarnings("unchecked")
    private <T> T doGetSingleton(String name, @Nullable Class<T> requiredType) throws NamingException {
        synchronized (this.singletonObjects) {
            if (this.singletonObjects.containsKey(name)) {
                Object jndiObject = this.singletonObjects.get(name);
                if (requiredType != null && !requiredType.isInstance(jndiObject)) {
                    throw new TypeMismatchNamingException(
                            convertJndiName(name), requiredType, (jndiObject != null ? jndiObject.getClass() : null));
                }
                return (T) jndiObject;
            }
            T jndiObject = lookup(name, requiredType);
            this.singletonObjects.put(name, jndiObject);
            return jndiObject;
        }
    }

    private Class<?> doGetType(String name) throws NamingException {
        if (isSingleton(name)) {
            return doGetSingleton(name, null).getClass();
        } else {
            synchronized (this.resourceTypes) {
                if (this.resourceTypes.containsKey(name)) {
                    return this.resourceTypes.get(name);
                } else {
                    Class<?> type = lookup(name, null).getClass();
                    this.resourceTypes.put(name, type);
                    return type;
                }
            }
        }
    }

}
